#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import re
import sys
import time
import json
import manpa
import socket
import shutil
import jinja2
import pathlib
import subprocess
import mirrors.plugin
from selenium.webdriver.common.by import By


class Main:

    def __init__(self, sock):
        self.sock = sock
        self.dataDir = mirrors.plugin.params["storage-file"]["data-directory"]
        self.wallpaperDir = os.path.join(self.dataDir, "data")
        self.i2pDir = os.path.join(self.dataDir, ".i2p")                        # FIXME

        self.p = InfoPrinter()
        self.failFlag = False

    def run(self):
        # read local file
        self.p.print("Read local files...")
        os.makedirs(self.wallpaperDir, exist_ok=True)
        localInfoDict = self._scanAndClearWallpaperDir()

        self.p.print("Staring I2P server...")
        os.makedirs(self.i2pDir, exist_ok=True)
        i2ps = TmpI2pServer(self.i2pDir)
        i2ps.start()
        self.p.print("Staring I2P server complete, port: %d." % (i2ps.get_proxy_port()))
        try:
            # FIXME: use this width/height so that "use cookie" dialog won't get in the way, it should be eliminated by manpa
            with manpa.Manpa(width=2500, height=2000, proxies=["http://127.0.0.1:%d" % (i2ps.get_proxy_port())]) as mp:
                mp.set_warning_callback(self._warnCallback)
                with mp.open_selenium_client() as driver:
                    startUrl = 'https://dynamicwallpaper.club/gallery?section=new'
                    while startUrl is not None:
                        # get remote url list
                        wallpaperUrlDict = dict()
                        self.p.print("Reading wallpapers...")
                        self.p.incIndent()
                        try:
                            driver.open_page(startUrl)
                            i = 0
                            while True:
                                self.p.print("Page %d: %s" % (i + 1, driver.current_url))
                                self.p.incIndent()
                                try:
                                    for aTag in driver.find_elements(by=By.XPATH, value=".//div[@class='gallery-item']//div[@class='wallpaper-info']//a"):
                                        wallpaperUrl = aTag.get_attribute("href")
                                        if os.path.basename(os.path.dirname(wallpaperUrl)) == "wallpaper" and os.path.basename(wallpaperUrl) not in localInfoDict:
                                            self.p.print("Wallpaper recorded: %s, %s" % (aTag.text, wallpaperUrl))
                                            wallpaperUrlDict[wallpaperUrl] = aTag.text                                  # wallpaper name may duplicate, so use url as key
                                finally:
                                    self.p.decIndent()

                                nextPageTagList = driver.find_elements(by=By.XPATH, value='.//a[@class="link bright featured"]')
                                if len(nextPageTagList) == 0:
                                    startUrl = None
                                    break

                                assert len(nextPageTagList) == 1
                                startUrl = nextPageTagList[0].get_attribute("href")

                                # too many wallpapers, we download them in several batches
                                if len(wallpaperUrlDict) > 10:
                                    break

                                driver.open_page(nextPageTagList[0].get_attribute("href"))
                                i += 1
                        finally:
                            self.p.decIndent()

                        # get wallpaper
                        self.p.print("Downloading wallpapers...")
                        self.p.incIndent()
                        try:
                            for url, name in wallpaperUrlDict.items():
                                self.p.print("Downloading wallpaper \"%s\" from \"%s\"..." % (name, url))

                                driver.open_page(url)

                                # FIXME: manpa should eliminate this dialog
                                buttonTagList = driver.find_elements(by=By.XPATH, value=".//button[@aria-label='Do not consent']")
                                if len(buttonTagList) > 0:
                                    assert len(buttonTagList) == 1
                                    driver.click_and_wait(buttonTagList[0])

                                # parse wallpaper tags
                                tags = []
                                for aTag in driver.find_elements(by=By.XPATH, value=".//a"):
                                    if aTag.get_attribute("href") is not None:
                                        m = re.fullmatch(".*&tag=(.*)$", aTag.get_attribute("href"))
                                        if m is not None:
                                            tags.append(m.group(1))

                                # get wallpaper link
                                aTagList = driver.find_elements(by=By.XPATH, value=".//a[contains(@class,'download')]")
                                if len(aTagList) != 1:
                                    self._failCallback("Strange that len(aTagList) is %d, not 1" % (len(aTagList)))
                                    continue

                                # download wallpaper
                                fullfn = driver.click_for_downloaded_file(aTagList[0])
                                info = WallpaperInfo(os.path.basename(url), tags, os.path.basename(fullfn))

                                # create wallpaper directory
                                saveDirFullfn = os.path.join(self.wallpaperDir, info.id)
                                os.mkdir(saveDirFullfn)
                                shutil.move(fullfn, os.path.join(saveDirFullfn, info.fn))
                                info.save_to_file(os.path.join(saveDirFullfn, "info.json"))

                                # record info
                                localInfoDict[info.id] = info
                        finally:
                            self.p.decIndent()
        finally:
            i2ps.stop()

        # generate www directory
        self.p.print("Generate www directory:")
        if True:
            selfDir = os.path.dirname(os.path.realpath(__file__))
            wwwDir = os.path.join(selfDir, "www")

            Util.forceDelete(os.path.join(self.dataDir, "css"))
            shutil.copytree(os.path.join(wwwDir, "css"), os.path.join(self.dataDir, "css"))

            # copy index page
            shutil.copy(os.path.join(wwwDir, "index.html"), os.path.join(self.dataDir, "index.html"))

            # generate detail pages
            for fn, info in localInfoDict.items():
                tm = jinja2.Environment(loader=jinja2.FileSystemLoader(wwwDir)).get_template("template-detail.html.jinja2")
                buf = tm.render({
                    "info": info,
                })
                with open(os.path.join(self.wallpaperDir, "detail.html"), "w") as f:
                    f.write(buf)

            # generate gallery page
            tm = jinja2.Environment(loader=jinja2.FileSystemLoader(wwwDir)).get_template("gallery.html.jinja2")
            buf = tm.render({
                "info_dict": localInfoDict,
            })
            with open(os.path.join(self.dataDir, "gallery.html"), "w") as f:
                f.write(buf)

        if self.failFlag:
            raise Exception("all Done, but failed")

        self.p.print("All Done.")

    def _scanAndClearWallpaperDir(self):
        ret = dict()

        for fn in os.listdir(self.wallpaperDir):
            fullfn = os.path.join(self.wallpaperDir, fn)

            if not Util.isRealDir(fullfn):
                Util.forceDelete(fullfn)
                continue

            jsonFullfn = os.path.join(fullfn, "info.json")
            if not Util.isRealFile(jsonFullfn):
                Util.forceDelete(fullfn)
                continue

            infoObj = None
            try:
                infoObj = WallpaperInfo.load_from_file(jsonFullfn)
            except BaseException:
                raise                   # FIXME: add try/catch, delete fullfn on error

            if infoObj.id != fn:
                Util.forceDelete(fullfn)
                continue

            wallpaperFileFullfn = os.path.join(fullfn, infoObj.fn)
            if not Util.isRealFile(wallpaperFileFullfn):
                Util.forceDelete(fullfn)
                continue

            ret[fn] = infoObj

        return ret

    def _failCallback(self, msg):
        self.p.print("FAILED: %s" % (msg))
        self.failFlag = True

    def _warnCallback(self, msg):
        self.p.print("WARNING: %s" % (msg))


class WallpaperInfo:

    def __init__(self, id, tags, fn):
        self.id = id
        self.tags = tags
        self.fn = fn

    def save_to_file(self, path):
        data = {
            "id": self.id,
            "tags": self.tags,
            "fn": self.fn,
        }
        with open(path, "w") as f:
            json.dump(data, f)

    @staticmethod
    def load_from_file(path):
        data = json.loads(pathlib.Path(path).read_text())
        if len(data) != 3:
            raise Exception("bad, %s" % (path))
        return WallpaperInfo(data["id"], data["tags"], data["fn"])


# FIXME: we should not use our own i2p server, and, manual filling self.i2pDir for bootstraping is needd
class TmpI2pServer:

    def __init__(self, i2pDir):
        self._tmppath = i2pDir
        self._proc = None

    def start(self):
        assert self._proc is None

        self._cfgFile = os.path.join(self._tmppath, "i2p.conf")
        self._pidFile = os.path.join(self._tmppath, "i2p.pid")
        self._logFile = os.path.join(self._tmppath, "i2p.log")
        self._tunnelsFile = os.path.join(self._tmppath, "tunnels.cfg")
        self._tunnelsDir = os.path.join(self._tmppath, "tunnels.d")

        self._proxyPort = Util.getFreeSocketPort("tcp")

        buf = ""
        buf += "tunconf = %s\n" % (self._tunnelsFile)
        buf += "tunnelsdir = %s\n" % (self._tunnelsDir)
        buf += "pidfile = %s\n" % (self._pidFile)
        buf += "logfile = %s\n" % (self._logFile)
        buf += "\n"
        buf += "ipv4 = true\n"
        buf += "ipv6 = true\n"
        buf += "ssu = true\n"
        buf += "\n"
        buf += "[ntcp2]\n"
        buf += "\n"
        buf += "[ssu2]\n"
        buf += "\n"
        buf += "[http]\n"
        buf += "enabled = false\n"
        buf += "\n"
        buf += "[httpproxy]\n"
        buf += "address = 127.0.0.1\n"
        buf += "port = %d\n" % (self._proxyPort)
        buf += "outproxy = http://outproxy.acetone.i2p:3128\n"
        buf += "outbound.length = 1\n"
        buf += "inbound.length = 1\n"
        buf += "\n"
        buf += "[sam]\n"
        buf += "enabled = true\n"
        buf += "\n"
        buf += "[bob]\n"
        buf += "\n"
        buf += "[i2cp]\n"
        buf += "\n"
        buf += "[i2pcontrol]\n"
        buf += "\n"
        buf += "[precomputation]\n"
        buf += "\n"
        buf += "[upnp]\n"
        buf += "enabled = true\n"
        buf += "name = I2Pd\n"
        buf += "\n"
        buf += "[meshnets]\n"
        buf += "\n"
        buf += "[reseed]\n"
        buf += "verify = true\n"
        buf += "\n"
        buf += "[addressbook]\n"
        buf += "\n"
        buf += "[limits]\n"
        buf += "\n"
        buf += "[trust]\n"
        buf += "\n"
        buf += "[exploratory]\n"
        buf += "\n"
        buf += "[persist]\n"
        buf += "\n"
        buf += "[cpuext]\n"
        buf += "\n"
        with open(self._cfgFile, "w") as f:
            f.write(buf)

        self._proc = subprocess.Popen(["i2pd", "--service", "--conf", self._cfgFile, "--datadir", self._tmppath],
                                      stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

        # FIXME: wait for the tunnels prepared
        time.sleep(10)

    def get_proxy_port(self):
        assert self._proc is not None
        return self._proxyPort

    def stop(self):
        assert self._proc is not None
        self._proc.terminate()
        self._proc.wait()
        self._proc = None


class Util:

    @staticmethod
    def isRealFile(path):
        return os.path.isfile(path) and not os.path.islink(path)

    @staticmethod
    def isRealDir(path):
        return os.path.isdir(path) and not os.path.islink(path)

    @staticmethod
    def forceDelete(path):
        if os.path.islink(path):
            os.remove(path)
        elif os.path.isfile(path):
            os.remove(path)
        elif os.path.isdir(path):
            shutil.rmtree(path)
        elif os.path.lexists(path):
            os.remove(path)             # other type of file, such as device node
        else:
            pass                        # path does not exist, do nothing

    @staticmethod
    def getFreeSocketPort(portType):
        if portType == "tcp":
            stlist = [socket.SOCK_STREAM]
        elif portType == "udp":
            stlist = [socket.SOCK_DGRAM]
        elif portType == "tcp+udp":
            stlist = [socket.SOCK_STREAM, socket.SOCK_DGRAM]
        else:
            assert False

        for port in range(10000, 65536):
            bFound = True
            for sType in stlist:
                s = socket.socket(socket.AF_INET, sType)
                try:
                    s.bind((('', port)))
                except socket.error:
                    bFound = False
                finally:
                    s.close()
            if bFound:
                return port

        raise Exception("no valid port")


class InfoPrinter:

    def __init__(self):
        self.indent = 0

    def incIndent(self):
        self.indent = self.indent + 1

    def decIndent(self):
        assert self.indent > 0
        self.indent = self.indent - 1

    def print(self, s):
        line = ""
        line += "\t" * self.indent
        line += s
        print(line)


###############################################################################

if __name__ == "__main__":
    with mirrors.plugin.ApiClient() as sock:
        try:
            Main(sock).run()
            sock.progress_changed(100)
        except Exception:
            sock.error_occured(sys.exc_info())
            raise
